﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace DynamicMocker
{
	/// <summary>
	/// Wrapper for create Fake method
	/// </summary>
	public class MethodWrapper
	{
		private Func<object[], object> _fake;
		private int _callCount;

		public MethodCallExpression OriginalMethodInfo { get; set; }

		#region Fakes for testing method which return some value
		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<TResult>(Func<TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, TResult>(Func<T1, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, TResult>(Func<T1, T2, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, TResult>(Func<T1, T2, T3, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, TResult>(Func<T1, T2, T3, T4, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, TResult>(Func<T1, T2, T3, T4, T5, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, TResult>(Func<T1, T2, T3, T4, T5, T6, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="T9"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="T9"></typeparam>
		/// <typeparam name="T10"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="T9"></typeparam>
		/// <typeparam name="T10"></typeparam>
		/// <typeparam name="T11"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="T9"></typeparam>
		/// <typeparam name="T10"></typeparam>
		/// <typeparam name="T11"></typeparam>
		/// <typeparam name="T12"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="T9"></typeparam>
		/// <typeparam name="T10"></typeparam>
		/// <typeparam name="T11"></typeparam>
		/// <typeparam name="T12"></typeparam>
		/// <typeparam name="T13"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="T9"></typeparam>
		/// <typeparam name="T10"></typeparam>
		/// <typeparam name="T11"></typeparam>
		/// <typeparam name="T12"></typeparam>
		/// <typeparam name="T13"></typeparam>
		/// <typeparam name="T14"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="T9"></typeparam>
		/// <typeparam name="T10"></typeparam>
		/// <typeparam name="T11"></typeparam>
		/// <typeparam name="T12"></typeparam>
		/// <typeparam name="T13"></typeparam>
		/// <typeparam name="T14"></typeparam>
		/// <typeparam name="T15"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}

		/// <summary>
		/// Return value
		/// </summary>
		/// <typeparam name="T1"></typeparam>
		/// <typeparam name="T2"></typeparam>
		/// <typeparam name="T3"></typeparam>
		/// <typeparam name="T4"></typeparam>
		/// <typeparam name="T5"></typeparam>
		/// <typeparam name="T6"></typeparam>
		/// <typeparam name="T7"></typeparam>
		/// <typeparam name="T8"></typeparam>
		/// <typeparam name="T9"></typeparam>
		/// <typeparam name="T10"></typeparam>
		/// <typeparam name="T11"></typeparam>
		/// <typeparam name="T12"></typeparam>
		/// <typeparam name="T13"></typeparam>
		/// <typeparam name="T14"></typeparam>
		/// <typeparam name="T15"></typeparam>
		/// <typeparam name="T16"></typeparam>
		/// <typeparam name="TResult"></typeparam>
		/// <param name="expression"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, TResult>(Func<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, TResult> expression)
		{
			return SetFake(expression.Method.GetParameters(), expression);
		}
		#endregion

		#region Fakes for testing method which have not return value
		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns(Action action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1>(Action<T1> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2>(Action<T1, T2> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3>(Action<T1, T2, T3> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4>(Action<T1, T2, T3, T4> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5>(Action<T1, T2, T3, T4, T5> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6>(Action<T1, T2, T3, T4, T5, T6> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7>(Action<T1, T2, T3, T4, T5, T6, T7> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8>(Action<T1, T2, T3, T4, T5, T6, T7, T8> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="action"></param>
		public MethodWrapper Returns<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16> action)
		{
			return SetFake(action.Method.GetParameters(), action);
		}

		#endregion

		private MethodWrapper SetFake(IList<ParameterInfo> parameters, Delegate func)
		{
			VerifyParameterList(parameters);
			_fake = (object[] args) => { return func.DynamicInvoke(args); };
			return this;
		}

		private void VerifyParameterList(IList<ParameterInfo> actualParams)
		{
			var expectedParams = OriginalMethodInfo.Method.GetParameters();
			var isCorrect = expectedParams.Length == actualParams.Count;

			if (isCorrect)
			{
				for (int i = 0; i < expectedParams.Length; i++)
				{
					if (!actualParams[i].ParameterType.IsAssignableFrom(expectedParams[i].ParameterType))
					{
						isCorrect = false;
						break;
					}
				}
			}

			if (!isCorrect)
			{
				throw new Exception("Invalid parameter list");
			}
		}

		public object Execute(params object[] args)
		{
			_callCount++;
			for (var i = 0; i < OriginalMethodInfo.Arguments.Count; i++)
			{
				var argument = OriginalMethodInfo.Arguments[i];
				if (argument.NodeType != ExpressionType.Call) continue;
				var callExpression = ((MethodCallExpression)argument);
				var methodInfo = callExpression.Method;
				if (methodInfo != null && methodInfo.IsStatic && methodInfo.DeclaringType.Assembly == Assembly.GetExecutingAssembly()
					&& methodInfo.DeclaringType.Name == "It")
				{
					var messageFormat = "";
					var type = args[i].GetType();
					object vObject = null;

					switch (methodInfo.Name)
					{
						case "IsAny":
							if (methodInfo.ReturnType != type)
								messageFormat = "Invalid parameter type in method {0}({1}{2}:{3}{4}), class: {5}, actual parameter type: {6}";
							break;
						case "IsEqualTo":
							vObject = ((ConstantExpression) callExpression.Arguments[0]).Value;
							if (methodInfo.ReturnType != type || methodInfo.ReturnType == type && !args[i].Equals(vObject))
							{
								vObject = vObject is string ? ("'" + vObject + "") : vObject;
								messageFormat = "Invalid parameter value in method {0}({1}{2}:{3}{4}), class: {5}, actual value: {7}, expected value: {8}";
							}
							break;
						case "IsMatchTo":
							vObject = ((ConstantExpression)callExpression.Arguments[0]).Value;
							var rExp = new Regex((string) vObject);
							if (!rExp.IsMatch((string) args[i]))
							{
								messageFormat = "Value '{7}' in parameters of method {5}.{0}({1}{2}:{3}{4}) did not match with pattern: '{8}'";
							}
							break;
						default:
							new Exception("Suitable method for parameters checking not found: " + methodInfo.Name);
							break;
					}

					if (messageFormat.Length > 0)
					{
						var method = OriginalMethodInfo.Method;
						var name = "";
						var argName = "";
						var argType = "";
						var className = "";

						if (method != null)
						{
							name = method.Name;
							var parameterInfo = method.GetParameters()[i];
							argName = parameterInfo.Name;
							argType = parameterInfo.ParameterType.ToString();
							className = method.DeclaringType.Name;
						}

						var message = string.Format(messageFormat,
						                            name,
						                            ".., ".Duplicate(i),
						                            argName,
						                            argType,
						                            ", ...".Duplicate(i),
						                            className,
						                            type,
						                            args[i],
																				vObject);

						throw new ArgumentException(message);
					}
				}
			}
			if (_callCount > 0 && ExpectedCallCount > 0 && ExpectedCallCount < _callCount)
				throw new Exception(string.Format("Calls number are exceeded, expected: {0}, actual: {1}", ExpectedCallCount,
				                                  _callCount));
			return _fake(args);
		}

		public MethodWrapper SetExpectedCallCount(int value)
		{
			ExpectedCallCount = value;
			return this;
		}

		public int ExpectedCallCount { get; set; }

		public int CallCount { get { return _callCount; } }

		public bool Verify()
		{
			return ExpectedCallCount > 0 && _callCount == ExpectedCallCount || ExpectedCallCount == 0;
		}
	}

	public static class StringExtention
	{
		public static string Duplicate(this string copy, int count)
		{
			return BuildString(count, copy);
		}

		private static string BuildString(int count, string copy)
		{
			var builder = new StringBuilder();
			for (int i = 0; i < count; i++)
			{
				builder.Append(copy);
			}
			return builder.ToString();
		}
	}
}